Aller au contenu principal

Accessibilité (a11y) et Internationalisation (i18n)

Accessibilité (a11y)

Concepts clés

  • Sémantique : fournir du contexte pour lecteurs d'écran
  • Contraste : texte lisible sur fond
  • Taille de police flexible : supporter les paramètres du système
  • Navigation clavier : être navigable sans souris/touch
  • Gestes alternatifs : ne pas dépendre d'un seul geste

Sémantique

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({super.key});

void onAdd() {
debugPrint('Ajout au panier');
}


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Accessibilité: Semantics')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Semantics(
label: 'Bouton pour ajouter au panier',
button: true,
child: IconButton(
onPressed: onAdd,
icon: const Icon(Icons.add_shopping_cart),
tooltip: 'Ajouter au panier',
),
),
const SizedBox(height: 12),
IconButton(
onPressed: onAdd,
icon: const Icon(Icons.add_shopping_cart),
tooltip: 'Ajouter au panier', // Lecteur d'écran le lira
),
],
),
),
),
);
}
}

Images accessibles

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({super.key});


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Image accessible')),
body: Center(
child: Image.asset(
'assets/logo.png',
semanticLabel: 'Logo de l\'application',
),
),
),
);
}
}

Texte adaptatif à la taille de l'appareil

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({super.key});


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Texte adaptatif')),
body: Builder(
builder: (context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Mon texte',
style: TextStyle(
fontSize: MediaQuery.of(context).textScaleFactor * 16,
),
),
const SizedBox(height: 12),
Text(
'Mon texte',
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
);
},
),
),
);
}
}

Focus et navigation clavier

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({super.key});


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Navigation clavier')),
body: Center(
child: Focus(
onKey: (node, event) {
if (event.isKeyPressed(LogicalKeyboardKey.enter)) {
debugPrint('Entrée pressée');
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: ElevatedButton(
onPressed: () {},
child: const Text('Bouton'),
),
),
),
),
);
}
}

Tests accessibilité rapides

  • VoiceOver (iOS) / TalkBack (Android) activés : vérifier que tout est annoncé
  • Taille police x1.5/x2 : vérifier overflow
  • Mode contrasté : vérifier lisibilité
  • Navigation clavier : utiliser Tab/Shift+Tab

Internationalisation (i18n)

Pourquoi i18n ?

  • Supporter plusieurs langues
  • Adapter formats (dates, devises, nombres)
  • Localisation des contenus

Setup avec flutter_gen_l10n

1. Configuration dans pubspec.yaml

flutter_localizations:
sdk: flutter

2. Créer fichiers ARB

lib/l10n/app_en.arb
lib/l10n/app_fr_CA.arb
lib/l10n/app_es.arb

app_en.arb :

{
"helloWorld": "Hello World",
"welcome": "Welcome {name}",
"@welcome": {
"description": "Accueil avec le nom"
}
}

app_fr_CA.arb :

{
"helloWorld": "Bonjour le monde",
"welcome": "Bienvenue {name}"
}

3. Générer locales dans pubspec.yaml

flutter:
generate: true

localization:
generate: true
enable-asserts: true

Lancer flutter gen-l10n pour générer les fichiers

4. Configuration de MaterialApp

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({super.key});


Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('fr', 'CA'), // Ou null pour suivre le système
home: const HomePage(),
);
}
}

class HomePage extends StatelessWidget {
const HomePage({super.key});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('i18n')),
body: Center(
child: Text(AppLocalizations.of(context)!.helloWorld),
),
);
}
}

5. Utiliser dans les widgets

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({super.key});


Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomePage(),
);
}
}

class HomePage extends StatelessWidget {
const HomePage({super.key});


Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: const Text('Textes localisés')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.helloWorld),
const SizedBox(height: 8),
Text(l10n.welcome(name: 'Alice')),
],
),
),
);
}
}

Formats localisés (dates, devises, nombres)

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({super.key});


Widget build(BuildContext context) {
final date = DateFormat.yMMMd('fr_CA').format(DateTime.now());
final currency = NumberFormat.simpleCurrency(locale: 'fr_CA').format(99.99);
final number = NumberFormat('#,##0.##', 'fr_CA').format(1234.567);

return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Formats localisés')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Date: $date'),
Text('Devise: $currency'),
Text('Nombre: $number'),
],
),
),
),
);
}
}

Changement de langue en temps réel

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
const MyApp({super.key});


State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
Locale _locale = const Locale('fr', 'CA');

void setLocale(Locale locale) {
setState(() => _locale = locale);
}


Widget build(BuildContext context) {
return MaterialApp(
locale: _locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: HomePage(onLocaleChange: setLocale),
);
}
}

class HomePage extends StatelessWidget {
final void Function(Locale) onLocaleChange;

const HomePage({super.key, required this.onLocaleChange});


Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: const Text('Changer la langue')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.helloWorld),
const SizedBox(height: 12),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () => onLocaleChange(const Locale('fr', 'CA')),
child: const Text('FR-CA'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => onLocaleChange(const Locale('en')),
child: const Text('EN'),
),
],
),
],
),
),
);
}
}

Bonnes pratiques

Accessibilité

  1. Toujours ajouter des labels : Semantics, Tooltip, semanticLabel
  2. Vérifier les contrastes : utiliser ColorScheme
  3. Tester avec lecteur d'écran : VoiceOver/TalkBack
  4. Support clavier : toutes les actions tactiles doivent être accessibles au clavier
  5. Tailles adaptatrices : respecter les paramètres de taille système

Internationalisation

  1. Utiliser ARB : format standard et facile à externaliser
  2. Pas de chaînes en dur : toujours via AppLocalizations
  3. Tester plusieurs locales : pas toutes les langues = même longueur
  4. Formats localisés : dates, devises, nombres
  5. RTL support : tester langues de droite à gauche (arabe, hébreu)